package com.wggd.bcreader;

import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;

import org.apache.commons.lang3.StringUtils;

public class BarReader {
	private static final String OUTPUT_DIRECTORY = "D:\\barcode-out\\";
	private static final Map<String, String> i25Map = new HashMap<String, String>();
	private List<String> results;
	
	static {
		i25Map.put("nnwwn", "0");
		i25Map.put("wnnnw", "1");
		i25Map.put("nwnnw", "2");
		i25Map.put("wwnnn", "3");
		i25Map.put("nnwnw", "4");
		i25Map.put("wnwnn", "5");
		i25Map.put("nwwnn", "6");
		i25Map.put("nnnww", "7");
		i25Map.put("wnnwn", "8");
		i25Map.put("nwnwn", "9");
	}
	private String inputImagePath;
	
	public BarReader(String inputImagePath) {
		this.inputImagePath = inputImagePath;
	}
	
	public String getBarcode() {
		BufferedImage image;
		
		try {
			image = ImageIO.read(new File(inputImagePath));
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
		
		outputThresholdedImage(image);
		
		this.results = new LinkedList<String>();
		
		int height = 0;
		for (; height < image.getHeight(); ++height) {
			
			height = 785;
			
			String rawOutput = new Scanline(image, height).toString();
			System.out.println("raw: " + rawOutput);
			
			String output = trim(rawOutput);
			System.out.println("trimmed: " + output);
//			System.out.println("trimmedSize: " + output.length());
			System.out.println("compr: " + compress(output));
	//		System.out.println("comprSize: " + compress(output).length());
			
			System.out.println("nbarSize: " + getNarrowBarSize(output));
			
			if (!isStartValid(output)) {
	//			System.err.println("invalid barcode start");
				continue;
			}
			if (!isEndValid(output)) {
	//			System.err.println("invalid barcode end");
				continue;
			}
			
			output = extractBars(output);
			System.out.println("extractedBars: " + output);
//			System.out.println("extractedBarsSize: " + output.length());
			
			if (!isInterleaved(output) || !isMinimumSize(output)) {
	//			System.err.println("not interleaved");
				continue;
			}
			
			output = barsToNumbers(output);
			System.out.println("numbers: " + output);
	//		System.out.println("numbersSize: " + output.length());
			
			if (output != null && output.length() > 10) {
				this.results.add(output);
			}
			
			if (output != null && output.length() > 0 && !output.contains("?")) {
				System.out.println(String.format("Height: %5d Read: %s --- from: %s", height, output, inputImagePath));
			}
			
			break;
		}
		
		sortResults(results);
		
		System.out.println("RESULTS --- " + inputImagePath);
		for (int i = 0; i < 10 && i < results.size(); ++i) {
			System.out.println(results.get(i));
		}
		
		return "";
	}

	private void sortResults(List<String> results) {
		Collections.sort(results, new Comparator<String>() {
			@Override
			public int compare(String a, String b) {
				return new Integer(b.length()).compareTo(a.length());
			}
		});
		
		Collections.sort(results, new Comparator<String>() {
			@Override
			public int compare(String a, String b) {
				return new Integer(StringUtils.countMatches(a, "?")).compareTo(StringUtils.countMatches(b, "?"));
			}
		});
	}
	
	private boolean isMinimumSize(String output) {
		return output != null && output.length() > 10;
	}

	private boolean isInterleaved(String bars) {
		boolean upper = Character.isUpperCase(bars.charAt(0));
		
		for (int i = 1; i < bars.length(); i++) {
			if (upper == Character.isUpperCase(bars.charAt(i))) {
				return false;
			} else {
				upper = !upper;
			}
		}
		
		return true;
	}

	private String barsToNumbers(String bars) {
		bars = clipStartEnd(bars);
		StringBuilder numbersBuilder = new StringBuilder();
		
		for (int start = 0; start + 9 < bars.length(); start += 10) {
			StringBuilder symbolBuilder1 = new StringBuilder();
			StringBuilder symbolBuilder2 = new StringBuilder();
			
			for (int i = 0; i < 5; ++i) {
				symbolBuilder1.append(bars.charAt(start + i*2));
				symbolBuilder2.append(bars.charAt(start + 1 + i*2));
			}
			
			String symbol1 = symbolBuilder1.toString().toLowerCase();
			String symbol2 = symbolBuilder2.toString().toLowerCase();
			String number1 = i25Map.get(symbol1);
			String number2 = i25Map.get(symbol2);
			if (number1 == null) {
				number1 = "?";
			}
			if (number2 == null) {
				number2 = "?";
			}
			numbersBuilder.append(number1);
			numbersBuilder.append(number2);
		}
		
		return numbersBuilder.toString();
	}
	
	private String clipStartEnd(String output) {
		return output.substring(4, output.length() - 3);
	}

	private String extractBars(String str) {
		int narrowBarSize = getNarrowBarSize(str);
		if (narrowBarSize == -1) {
			return null;
		}
		
		String[] bars = splitBars(str);
		
		for (int i = 0; i < bars.length; i++) {
			bars[i] = translateBar(bars[i], narrowBarSize);
		}
				
		return StringUtils.join(bars);
	}
	
	private String translateBar(String bar, int narrowBarSize) {
		String s = bar.length() <= narrowBarSize
				   ? "n"
				   : "w";
		
		if (bar.charAt(0) == '1') {
			s = s.toUpperCase();
		}
		
		return s;
	}
	
	private String[] splitBars(String str) {
		List<String> bars = new LinkedList<String>();
		
		char lastCh = str.charAt(0);
		int start = 0;
		for (int i = 1; i < str.length(); i++) {
			char ch = str.charAt(i);
			if (ch != lastCh) {
				bars.add(str.substring(start, i));
				lastCh = ch;
				start = i;
			}
		}
		bars.add(str.substring(start));		
		
		return bars.toArray(new String[bars.size()]);
	}

	private int getNarrowBarSize(String str) {
		Matcher m = Pattern.compile("^[1]+[0]+[1]+[0]+").matcher(str);
		m.find();
		String startSeq;
		
		try {
			startSeq = m.group();
		} catch(IllegalStateException ex) {
			System.err.println("no barcode start here");
			return -1;
		}
		
		int[] startSeqWidthBounds = findWidthBounds(startSeq);
//		System.out.println("startSeqWidthBounds:" + Arrays.toString(findWidthBounds(startSeq)));
		
		return (int) (startSeqWidthBounds[1] + (startSeqWidthBounds[1] * 1.5));
	}
	
	private boolean isStartValid(String str) {
		return str != null && str.length() > 0 && compress(str).startsWith("1010");
	}
	
	private boolean isEndValid(String str) {
		return str != null && str.length() > 0 && compress(str).endsWith("101");
	}

	private String compress(String str) {
		StringBuilder builder = new StringBuilder();
		
		char lastCh = str.charAt(0);
		for (int i = 1; i < str.length(); i++) {
			char ch = str.charAt(i);
			if (ch != lastCh) {
				builder.append(lastCh);
				lastCh = ch;
			}
		}
		builder.append(lastCh);
		
		return builder.toString();
	}
	
	private int[] findWidthBounds(String line) {
		int[] bounds0 = findWidthBounds(line, "0");
		int[] bounds1 = findWidthBounds(line, "1");
		
		return new int[] {Math.min(bounds0[0], bounds1[0]), Math.min(bounds0[1], bounds1[1])};
	}
	
	private int[] findWidthBounds(String line, String separators) {
		int minWidth = Integer.MAX_VALUE;
		int maxWidth = Integer.MIN_VALUE;
		
		for (String s : line.split(separators)) {
			if (s.length() > 0) {
				if (s.length() < minWidth) {
					minWidth = s.length();
				}
				
				if (s.length() > maxWidth) {
					maxWidth = s.length();
				}
			}
		}
		
		return new int[] {minWidth, maxWidth}; 
	}
	
	private String trim(String rawBarcode) {
		return StringUtils.strip(rawBarcode, "0");
	}
	
	private void outputThresholdedImage(BufferedImage image) {
		image = cloneBufferedImage(image);
		
		String outputPath = OUTPUT_DIRECTORY + "out" + inputImagePath.substring(inputImagePath.lastIndexOf('\\') + 1);
		
		for (int y = 0; y < image.getHeight(); ++y) {
			int[] bwRGB = new Scanline(image, y).getThresholdedRGB();
			for (int i = 0; i < bwRGB.length; i++) {
				bwRGB[i] = bwRGB[i] * 0xffffff;
			}
			image.setRGB(0, y, image.getWidth(), 1, bwRGB, 0, 1);
		}
				
		try {
			ImageIO.write(image, "jpeg", new File(outputPath));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private BufferedImage cloneBufferedImage(BufferedImage bi) {
		ColorModel cm = bi.getColorModel();
		boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
		WritableRaster raster = bi.copyData(null);
		return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
	}
}
